概述
本文基于原生 Android S 代码。
事件分发流程
事件的处理主要有下面几个类:
PhoneStatusBarView 主要处理从状态栏下拉通知面板
OverviewProxyService 主要操作桌面下拉通知面板
NotificationShadeWindowViewController 主要处理锁屏切换下拉通知的操作。
NotificationPanelViewController & PanelViewController 主要处理 QS Panel 的整体操作,比如显示,隐藏和整体滑动等等。
NotificationStackScrollLayoutController 主要处理通知中心的滑动,它处理事件时会通知 NotificationPanelViewController 更新 QS 的可见高度。
PhoneStatusBarView
从状态栏下拉时,事件由 PhoneStatusBarView 分发给 NotificationPanelView 来处理面板的整体滑动。
1 | //PanelBar.java |
OverviewProxyService
OverviewProxyService 监听了 Launcher 里面的下滑事件,在处理DOWN事件时设置 shader view 可见,那么后面shader view就可以分发和处理事件了。
具体看下面桌面下拉部分的介绍。
NotificationShadeWindowView
NotificationShadeWindowView 处理当它可见时对所有事件的分发,以及对锁屏状态下的下拉(非状态栏下拉)事件的处理。
NotificationPanelView
NotificationPanelView 主要处理QS完全展开与QS折叠之间状态下的事件处理,可以调用通知中心接口来移动通知中心。PanelView 主要是进行QS面板的整体操作,比如显示,隐藏和整体滑动等等。
NotificationPanelView 和它的父类 PanelView 分别设置了事件拦截器和OnTouchListener,事件的处理工作主要由 NotificationPanelViewController 和 PanelViewController 创建的 TouchHandler() 来处理。
1 | public void setOnTouchListener(PanelViewController.TouchHandler touchHandler) { |
NotificationPanelView 对事件拦截:
1 | NotificationShadeWindowView.dispatchTouchEvent() |
在大部分场景下,Down 事件一般都不是 NotificationPanelView 消费的,如果 NotificationPanelViewController.TouchHandler.onInterceptTouchEvent()
这里拦截了,就处理QSPanel的整体操作,比如显示和隐藏等。不拦截了就向下分发,处理通知中心折叠操作等。
1 | NotificationPanelViewController.TouchHandler.onTouch() |
关于 onTouch 部分的操作,可以看下面QS滑动部分有详细介绍。
NotificationStackScrollLayout
处理三种类型的事件:1.单个通知的展开和收缩手势,2.通知中心的滑动和滚动(这里所说的滑动和滚动,意在区分不同场景下通知中心的滚动),3.左右滑动删除通知操作
1 | public boolean onInterceptTouchEvent(MotionEvent ev) { |
1 | public boolean onTouchEvent(MotionEvent ev) { |
1 | public boolean onInterceptTouchEvent(MotionEvent ev) { |
1 | NotificationStackScrollLayout.onInterceptTouchEvent() |
1 | //NotificationStackScrollLayoutController.java |
1 | NotificationStackScrollLayout.onTouchEvent() |
QSPanel 的几种显示场景
NotificationStackScrollLayout 的三级折叠
0.QSPanel隐藏
1.通知中心全部显示
2.显示QQS和通知中心
3.显示QS,不显示通知中心
2到1的切换有个条件就是通知中心在2状态下时满屏的,可以滚动,这时的通知栏滑动处理成ScrollView的滚动问题。
一个比较重要的方法就是 NotificationPanelViewController.canCollapsePanelOnTouch()
1 | protected boolean canCollapsePanelOnTouch() { |
在 PanelViewController.TouchHandler.onInterceptTouchEvent()
中相应 MotionEvent.ACTION_MOVE
做判断,如果通知中心不能滚动,就拦截 MotionEvent.ACTION_MOVE
事件,做 QSPanel 上移或者隐藏的动作。
如果通知中心可以滚动,那么就不拦截MotionEvent.ACTION_MOVE
事件,交给通知中心做滚动处理。
1 | // PanelViewController.java |
NotificationStackScrollLayout.onInterceptTouchEventScroll 收到 MotionEvent.ACTION_MOVE
事件后,会做相应的判断,做事件的拦截:
1 | boolean onInterceptTouchEventScroll(MotionEvent ev) { |
然后事件就交给 NotificationStackScrollLayoutController.TouchHandler.onTouchEvent() -> NotificationStackScrollLayout.onScrollTouch() -> NotificationStackScrollLayout.customOverScrollBy()
处理上划滚动。
2->3 通知中心下滑,这时 QSPanel不对 MotionEvent.ACTION_MOVE
做拦截,这时交给 NotificationStackScrollLayout 来处理向下滚动的 MotionEvent.ACTION_MOVE
和 MotionEvent.ACTION_UP
事件。
2->0
MotionEvent.ACTION_UP
事件后包含两种场景,一个是QSPanel消失,另外一个时回弹回2场景。这两种情况都是由 PanelViewController.TouchHandler.onTouch
中对 MotionEvent.ACTION_UP
的处理。
然后在 PanelViewController.endMotionEvent
方法中通过 flingExpands()
来计算是否 expand,根据expand来计算最终的位置。如果expand为true就回原位,否则上划消失。
1 | protected boolean flingExpands(float vel, float vectorVel, float x, float y) { |
0->2 切换时也由PanelViewController处理,此时满足 expand 条件,就会去做 expand 动画。
3->2 切换,在QS上做上划动作,NotificationStackScrollLayout 不消费DOWN事件,被NotificationPanelView子View消费,后续MOVE 和 UP事件事件再经过 NotificationPanelView 被拦截,被 NotificationPanelView.onTouch() -> NotificationPanelViewController.handleQsTouch()->onQsTouch() 处理。
状态栏下拉
单指滑动
从状态栏下拉时,事件由 PhoneStatusBarView 分发给 NotificationPanelView 来处理面板的整体滑动。此时的事件不经过NotificationShadeWindowView分发。
在这种情况下,PanelViewController.TouchHandler.onInterceptTouchEvent() 和 NotificationPanelViewController.TouchHandler.onTouch()在Down事件时返回true,消费该事件,那么后面的Move和UP事件也会在这里或者父类的onTouch()中处理。
1 | StatusBarWindowView.dispatchTouchEvent() |
1 | // PanelViewController.java |
双指滑动
这个场景指的是双指从状态栏下拉,和单指下拉的区别是单指下拉后的状态是显示QQS和通知中心,双指下拉的最终状态时显示QS。
具体的事件分发流程就和上面单指状态栏下拉是一样的,这里就不多介绍。只介绍一下双指下拉的不一样的流程。
这个场景中应用的两个比较重要的变量是 mTwoFingerQsExpandPossible 和 mQsExpandImmediate。
在下面的方法中进行赋值:
1 | private boolean handleQsTouch(MotionEvent event) { |
1 | private boolean isOpenQsEvent(MotionEvent event) { |
滑动过程中
move 的时候用getMaxPanelHeight()计算 panel 高度,双指滑动时计算最大高度是用 calculatePanelHeightQsExpanded(),单指滑动是用 calculatePanelHeightShade()。具体看后面文章介绍。
onHeightUpdated 时双指滑动会同时调用 positionClockAndNotifications() 来更新通知中心位置以及setQsExpansion()来设置QS展开高度。单指滑动时在这里只调用positionClockAndNotifications()不调用setQsExpansion()。
这里的 setQsExpansion() 就是用QQS高度加上展开进度乘以最大展开高度(完全展开QS)来计算的。
而单指滑动这时因为最终状态是不显示QS的,因此没有主动调用的必要。也就是NotificationPanelViewController.在onLayoutChange()会附带的调用一下,设置的高度也是定值mQsMinExpansionHeight。1
2
3
4
5
6
7
8
9//NotificationPanelViewController.java
private boolean handleQsTouch(MotionEvent event) {
....
if (!mQsExpandImmediate && mQsTracking) {
onQsTouch(event);
if (!mConflictingQsExpansionGesture) {
return true;
}
}
1 | //NotificationPanelViewController.java |
1 | @Override |
fling 的时候用getMaxPanelHeight()计算 panel 高度,双指滑动时计算最大高度是用 calculatePanelHeightQsExpanded(),单指滑动是用 calculatePanelHeightShade()。具体看后面文章介绍。
因此,我们可以看到,关键位置主要是根据 mQsExpandImmediate 在Move的时候额外调用了 setQsExpansion() 来设置QS展开,在Fling时计算 getMaxPanelHeight() 时根据 mQsExpandImmediate 做了处理。
桌面下拉
桌面下拉事件处理逻辑分为两种情况,一种是 OverviewProxyService 自己处理,另外一种是 OverviewProxyService + NotificationPanelViewController 来处理。
首先 OverviewProxyService 接收DOWN事件后,设置shade view可见。
第一种情况是 OverviewProxyService 自己处理,这种情况下可能是在手势很快的情况下,事件没有分发到 NotificationPanelView,那么就OverviewProxyService自己处理。
OverviewProxyService 只处理 DONW, UP和CANCEL事件。
1 | OverviewProxyService.onStatusBarMotionEvent() |
第二种情况是OverviewProxyService + NotificationPanelViewController 来处理。因为已经设置了shade view可见,然后它就可以接收事件开始处理动画了。此时后面的Down,Move,Up事件都由 shade view 处理了,那么它会OverviewProxyService 收到一个CANCEL事件。
接下来就是 NotificationPanelViewController 接收Down和Move事件来处理通知面板的整体滑动操作。和状态栏下拉处理比较类似,具体看上面介绍。这种情况下会在 shouldGestureWaitForTouchSlop() 中设置 mExpectingSynthesizedDown = false,那么OverviewProxyService收到cancel事件时就不会处理fling。
如果 OverviewProxyService 没有收到 CANCEL事件,表示 NotificationPanelViewController 没有来得及去处理事件,那么就在OverviewProxyService.ACTION_UP中处理下拉面板的状态,这就是第一种情况。
现在来介绍另外一个小知识:为什么同一个事件触发流程会在两个窗口之间跨窗口传递。是因为桌面在向SystemUI传递Down事件时,通过 setWindowSlippery()
方法为桌面窗口设置了FLAG_SLIPPERY 属性。设置了这个属性后,如果设置了SystemUI 的View可见,就相当于覆盖在了桌面上面,那么这个事件就会向当前页面(桌面)发送CANCEL事件,把事件分发给覆盖的窗口–SystemUI进行处理。
滑动QS
这个场景表示在下面场景下的操作:
1.显示QQS和通知中心的情况下滑动通知中心以外区域
2.全部显示QS时在QS面板上面滑动。
这两种情况下,NotificationStackScrollLayout对DONW事件都不做处理。NotificationPanelViewController 对 Move 事件拦截,然后 Move和Up事件由 NotificationPanelViewController 或者 PanelViewController 处理。
有涉及到通知中心位移时就在 NotificationPanelViewController 中处理。处理下拉面板整体操作时就在 PanelViewController 中处理。
主要进行一下的逻辑操作:
1.设置QS显示高度
2.更新QQS的可见性
3.设置QS的绘制区域
4.更新通知中心的偏移
5.更新面板可见性
1 | PanelView.onTouchEvent() |
滑动通知中心
1.上滑
2.下滑
3.包含先上滑后再下滑:和第二种情况一样
前面也讲过,滑动通知中心也分两种情况,一种是上划做下拉面板的整体操作,一种是下滑,隐藏通知中心显示QS操作。
这两种情况下的Down事件,要么被 ExpandableNotificationRow 消费,满足下面条件且设置了ExpandableNotificationRow 设置了 setOnClickListener。
1 | // ExpandableNotificationRow.java |
要么ExpandableNotificationRow没消费,会被 NotificationStackScrollLayoutController.onTouchEvent() 消费。总之,NotificationStackScrollLayout 及其子View是可以消费Down事件的,那么后面的事件还会向它做分发。
但是 PanelViewController 在分发MOVE事件时,由于满足了下面的条件,对MOVE事件做了拦截和消费。那么接下来就是由 PanelViewController 来处理事件进行下拉面板的整体操作。
1 | PanelViewController.java |
这种情况下和上面介绍的 QS 滑动流程类似,就不再介绍。
第二种情况时,NotificationStackScrollLayout 在处理 ACTION_MOVE 事件时,由于满足了下面的条件,因此 NotificationStackScrollLayout 对Move事件做了拦截。而且这种情况下会设置 requestDisallowInterceptTouchEvent(true),阻止父组件对事件的拦截。那么后面的Move和Up事件就由NotificationStackScrollLayout来处理通知中心的滑动。
由于阻止了父组件的事件拦截,因此如果我们先滑动通知下滑然后再上滑是无法收起下拉面板的。
1 | NotificationStackScrollLayout.java |
1 | NotificationShadeWindowView.dispatchTouchEvent() |
锁屏下拉通知栏
状态栏下滑
这时的最终的状态可以是(3)显示QS,不显示通知中心。处理QS的滑动。
NotificationPanelViewController 没有拦截事件,NotificationStackScrollLayout 也没有消费,那么Down事件还是给 NotificationPanelView 来消费,以及后面的 Move 事件也被NotificationPanelView 来消费。
虽然在 NotificationPanelViewController.onInterceptTouchEvent 没有拦截,但是在 NotificationPanelViewController.onQsIntercept 处理Down事件时设置了 mView.getParent().requestDisallowInterceptTouchEvent(true)
,那么它的父组件们就不会再拦截了。
下面来看一下Down事件的分发流程
1 | NotificationShadeWindowView.dispatchTouchEvent() |
那么在什么情况下会拦截Down事件呢?
1 | private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) { |
此时 NotificationPanelViewController.TouchHandler.onTouch() 中的 handleQsTouch 是返回 true 的。
handleQsTouch() 方法来更新 QS 的显示高度以及更新通知中心的位置。
通知栏和其他区域下滑
这时的最终的状态可以是(2)显示QQS和通知中心,处理面板的整体滑动。
这种场景下的Down事件依旧是由 NotificationStackScrollLayout 或者其子 View ExpandableNotificationRow 消费。
但是 NotificationShadeWindowView 会拦截 ACTION_MOVE
事件,那么由 NotificationShadeWindowView.onTouchEvent() 来处理和消费 ACTION_MOVE
事件。
1 | NotificationShadeWindowView.onInterceptTouchEvent() |
如果触电在通知上,或者 isDragDownAnywhereEnabled 为true的情况下,都是可以下拉的。
1 | internal val isDragDownAnywhereEnabled: Boolean |
1 | StatusBar.getStatusBarWindowTouchListener |
锁屏上滑解锁
通知栏上滑
这个场景其实和上面介绍的滑动通知中心场景下上滑收起下拉面板的流程是一样的。
NotificationStackScrollLayout 或其子 View 消费了 DOWN 事件,但是PanelViewController 拦截并消费了 MOVE 事件,后面的 UP 事件也由它进行处理。
1 | NotificationShadeWindowView.dispatchTouchEvent() |
其他区域上滑
NotificationPanelView 的子 View 对 Down 事件都没有消费,那么最终是 NotificationPanelViewController.TouchHandler.onTouch() 消费了 Down 事件,PanelViewController 处理后面的 Move 和 Up 事件。
这个场景和滑动 QS 的流程是一样的。
解锁
点击导航栏收起
1 | StatusBar.onReceive() |